extends Control

enum RESULTS_STATE {IDLE, NOT_IDLE}

# The time between acting on buffered signals
const SIGNAL_BUFFER_TIMEOUT: float = 0.5

export(PackedScene) var SearchOptions_scene: PackedScene = load("res://src/scenes/search_options/search_options.tscn")
export(PackedScene) var MediaListItem_scene: PackedScene = load("res://src/scenes/media_list_item/media_list_item.tscn")
export(PackedScene) var ChannelCard_scene: PackedScene = load("res://src/scenes/channel_card/channel_card.tscn")
export(PackedScene) var ButtonContainer_scene: PackedScene = load("res://src/scenes/button_container/button_container.tscn")

var _results_state: int = RESULTS_STATE.IDLE
var _last_claim_search_args: Dictionary = {}
var _last_search_options_state: Dictionary = {}
var _last_page_loaded: int = 0
var _last_result_displayed: int = -1
var _exhausted_results: bool = false
var _displayed_exhausted_results: bool = false
var _results: Array = []
# Flags that are set to `true` when certain signals have been buffered,
# indicating that the actions the signals should cause should be executed when
# the buffer timer timeouts
var _make_scrollbar_visible_flag: bool = false
# State this scene should be initialized with
var _init_state: Dictionary = {}
var _is_ready: bool = false
# This timer is used to buffer signals that are emitted too frequently so their
# callbacks can be executed as if only 1 signal was emitted, reducing
# computation. When the timer timeouts, it will execute the callbacks of the
# emitted signals
var _signal_buffer_timer := Timer.new()
var _async_helper := AsyncHelper.new(self)
onready var _search_options: GridContainer = SearchOptions_scene.instance()
onready var _main_region: MainRegion = get_parent()
onready var _results_node: Container = find_node("Results")
onready var _results_scroll_container: ScrollContainer = \
		find_node("ResultsScrollContainer")
onready var _results_margin_container: MarginContainer = \
		find_node("ResultsMarginContainer")
onready var _search_bar: LineEdit = find_node("SearchBar")
onready var _search_hbox_container: HBoxContainer = \
		find_node("SearchHBoxContainer")
onready var _search_options_container: PanelContainer = \
		find_node("SearchOptionsContainer")
onready var _search_options_button: Button = find_node("SearchOptionsButton")


func _ready() -> void:
	var tracker: Dictionary = _async_helper.connect_wrapped(
			SDK.wait_until_ready(), "completed", false)
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	
	connect("visibility_changed", self, "_on_visibility_changed")
	_results_node.connect(
			"resized", self, "_on_Results_resized")
	_results_scroll_container.connect("v_scrollbar_reached_end",
			self, "_on_Results_v_scrollbar_reached_end")
	_search_bar.connect("text_entered", self, "_on_SearchBar_text_entered")
	_search_options_button.connect(
			"pressed", self, "_on_SearchOptionsButton_pressed")
	_signal_buffer_timer.connect(
			"timeout", self, "_on_signal_buffer_timer_timeout")
	
	_results_node.set_item_size(MediaListItem.SIZE)
	
	_signal_buffer_timer.set_wait_time(SIGNAL_BUFFER_TIMEOUT)
	_signal_buffer_timer.set_autostart(true)
	add_child(_signal_buffer_timer)
	
	if _init_state.empty():
		_setup()
	else:
		_setup_state()


func get_tab_title() -> String:
	return "Home"


func get_state() -> Dictionary:
	"""
	Returns:
		The data needed to restore the main changes made to this scene
	"""
	var state: Dictionary = {
		"_last_claim_search_args": _last_claim_search_args,
		"_last_page_loaded": _last_page_loaded,
		"_last_search_options_state": _last_search_options_state,
		"SearchOptionsContainer_visible": \
				_search_options_container.is_visible(),
		"ResultsScrollContainer_v_scroll_position": \
				_results_scroll_container.get_v_scrollbar().get_value(),
	}
	return state


func set_state(state: Dictionary) -> void:
	"""
	Call before the node enters the SceneTree to initialize it with the given
	state
	"""
	_init_state = state


func _setup() -> void:
	_search_options_container.add_child(_search_options)
	yield(_set_initial_args(), "completed")
	_make_scrollbar_visible_flag = true
	_is_ready = true


func _setup_state() -> void:
	""" Sets up the scene using the state description in `_init_state` """
	_last_claim_search_args = _init_state["_last_claim_search_args"]
	_search_bar.set_text(_last_claim_search_args.get("text", ""))
	
	_search_options.set_state(_init_state["_last_search_options_state"])
	_search_options_container.add_child(_search_options)
	_search_options_container.set_visible(
			_init_state["SearchOptionsContainer_visible"])
	
	_is_ready = true
	
	while _last_page_loaded < max(_init_state["_last_page_loaded"], 1):
		yield(_add_results(_last_claim_search_args), "completed")
	
	var tracker: Dictionary = _async_helper.connect_wrapped(
			get_tree(), "idle_frame", false)
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	_results_scroll_container.get_v_scrollbar().set_value(
			_init_state["ResultsScrollContainer_v_scroll_position"])


func _set_initial_args() -> void:
	"""
	If some channels are followed then sets `_last_claim_search_args` to the
	result of `_get_followed_args()`, otherswise to the results of
	`_get_trending_args`
	"""
	var tracker: Dictionary = _async_helper.connect_wrapped(
			LBRYActions.get_followed_channels(), "completed")
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	var followed_channels: Array = tracker["result"]
	
	var args: Dictionary
	if len(followed_channels) > 0:
		args = yield(_get_followed_args(), "completed")
	else:
		args = _get_trending_args()
	_last_claim_search_args = args


func _add_results(args: Dictionary, ignore_state: bool=false) -> void:
	if _results_state == RESULTS_STATE.IDLE or ignore_state:
		if not ignore_state:
			_results_state = RESULTS_STATE.NOT_IDLE
		
		_results += yield(_get_media(args), "completed")
		_display_media()
		
		if not ignore_state:
			_results_state = RESULTS_STATE.IDLE


func _add_results_until_scrollbar_visible(ignore_state: bool=false) -> void:
	"""
	Ensure the number of items displayed is enough to make the scrollbar
	visible (because dragging the scrollbar to the bottom is what makes more
	items load)
	"""
	# Must have `yield` here so that a `GDScriptFunctionState` is returned on
	# all paths
	var tracker: Dictionary = _async_helper.connect_wrapped(
			get_tree(), "idle_frame", false)
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	if _is_ready and (_results_state == RESULTS_STATE.IDLE or ignore_state):
		if not ignore_state:
			_results_state = RESULTS_STATE.NOT_IDLE
		
		while (
				is_visible_in_tree()
				and not _results_scroll_container \
						.get_v_scrollbar() \
						.is_visible_in_tree()
				and not _displayed_exhausted_results
		):
			yield(_add_results(_last_claim_search_args, true), "completed")
		
		if not ignore_state:
			_results_state = RESULTS_STATE.IDLE


func _apply_search(search_text: String, ignore_state: bool=false) -> void:
	"""
	Performs a search based on the text in the search box and the selected
	search options
	"""
	if _results_state == RESULTS_STATE.IDLE or ignore_state:
		if not ignore_state:
			_results_state = RESULTS_STATE.NOT_IDLE
		
		if LBRYActions.is_lbry_url(search_text):
			var opened: bool = yield(_open_lbry_url(search_text), "completed")
			if not opened:
				# Clear then display so that a message is shown saying that no
				# results were found
				_clear()
				_display_media()
		else:
			_clear()
			var args: Dictionary = yield(
					_get_searched_args(search_text), "completed")
			yield(_add_results(args, true), "completed")
			_make_scrollbar_visible_flag = true
		
		if not ignore_state:
			_results_state = RESULTS_STATE.IDLE


func _get_trending_args() -> Dictionary:
	var args: Dictionary = {
		"claim_type": "stream",
		"order_by": ["trending_score"],
		"no_totals": true,
		"include_is_my_output": true,
		"remove_duplicates": true,
	}
	return args


func _get_followed_args() -> Dictionary:
	var tracker: Dictionary = _async_helper.connect_wrapped(
			LBRYActions.get_followed_channels(), "completed")
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	var followed_channels: Array = tracker["result"]
	var channel_claim_ids: Array = []
	for channel in followed_channels:
		channel_claim_ids.append(channel.rsplit("#", true, 1)[1])
	var args: Dictionary = {
		"claim_type": "stream",
		"channel_ids": channel_claim_ids,
		"order_by": ["trending_score"],
		"no_totals": true,
		"include_is_my_output": true,
		"remove_duplicates": true,
	}
	return args


func _get_searched_args(search_text: String) -> Dictionary:
	var tracker: Dictionary = _async_helper.connect_wrapped(
			_search_options.get_options(), "completed")
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	var search_options: Dictionary = tracker["result"]
	var args: Dictionary = {
		"order_by": ["trending_score"],
		"text": search_text,
		"no_totals": true,
		"valid_channel_signature": true,
		"include_is_my_output": true,
		"remove_duplicates": true,
	}
	for key in search_options:
		args[key] = search_options[key]
	return args


func _get_media(args: Dictionary) -> Array:
	"""
	Performs a claim search using the given `args` on the page after the last
	page that was loaded. Also removes any results owned by the user
	"""
	_update_last_args(args)
	_last_search_options_state = _search_options.get_state()
	var media: Array = []
	# For some reason this yield gets stuck unless a container is resized
	var size_flag: int = _search_hbox_container.get_v_size_flags()
	_search_hbox_container.set_v_size_flags(SIZE_EXPAND)
	var tracker: Dictionary = _async_helper.connect_wrapped(
			get_tree(), "idle_frame", false)
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	_search_hbox_container.set_v_size_flags(size_flag)
	if not _exhausted_results:
		# Merge the permanent search options with the selected options
		var permanent_search_options: Dictionary = Data.get_search_options()
		Helpers.dictionary_merge_into(
				args, permanent_search_options, true, false)
		args["page"] = _last_page_loaded + 1
		
		tracker = _async_helper.connect_wrapped(
				SDK.claim_search(args), "completed")
		yield(_async_helper.wait_until_finished([tracker]), "completed")
		var search_response: Dictionary = tracker["result"]
		media = search_response.get("result", {}).get("items", [])
		
		# Remove media owned by the current account
		for i in range(len(media) - 1, -1, -1):
			if media[i].get("is_my_output", false) == true:
				media.remove(i)
		
		if len(media) == 0:
			_exhausted_results = true
		_last_page_loaded += 1
	return media


func _display_media() -> void:
	var results_slice = \
			_results.slice(_last_result_displayed + 1, len(_results) - 1)
	if len(results_slice) == 0 and not _displayed_exhausted_results:
		var label := Label.new()
		label.set_text("No more results")
		_results_node.add_child(label)
		_displayed_exhausted_results = true
	
	for item in results_slice:
		var item_type: String = item.get("value_type", "")
		if item_type == "channel":
			var item_node: ButtonContainer = ButtonContainer_scene.instance()
			var channel_card: ChannelCard = ChannelCard_scene.instance()
			var channel_url: String = item.get("short_url", "")
			
			item_node.set_h_size_flags(SIZE_EXPAND_FILL)
			item_node.set_normal_color(Color("3c3c46"))
			item_node.set_hovered_color(Color("50505a"))
			item_node.set_pressed_color(Color("32323c"))
			channel_card.init(channel_url)
			
			if not item_node.is_connected(
					"pressed", self, "_open_channel_window"):
				item_node.connect(
						"pressed", self, "_open_channel_window", [channel_url])
			
			item_node.add_child(channel_card)
			_results_node.add_child(item_node)
			_last_result_displayed += 1
		elif item_type == "stream":
			var item_node: MediaListItem = MediaListItem_scene.instance()
			var channel_url: String = item \
					.get("signing_channel", {}) \
					.get("short_url", "")
			item_node.init(item)
			
			if not item_node.is_media_button_connected(
					"pressed", self, "_open_media_window"):
				item_node.connect_media_button(
						"pressed", self, "_open_media_window", [item])
			if not item_node.is_channel_button_connected(
					"pressed", self, "_open_channel_window"):
				item_node.connect_channel_button(
						"pressed", self, "_open_channel_window", [channel_url])
			
			_results_node.add_child(item_node)
			_last_result_displayed += 1


func _clear() -> void:
	""" Removes all the results """
	for child in _results_node.get_children():
		if child.get_child_count() > 0 and child.get_child(0) is ChannelCard:
			var channel_card: ChannelCard = child.get_child(0)
			child.remove_child(channel_card)
		child.queue_free()
	_results.clear()
	_last_page_loaded = 0
	_last_result_displayed = -1
	_displayed_exhausted_results = false


func _update_last_args(args: Dictionary) -> bool:
	var changed: bool = false
	if args.hash() != _last_claim_search_args.hash():
		_last_page_loaded = 0
		_exhausted_results = false
		changed = true
	_last_claim_search_args = args
	return changed


func _open_lbry_url(lbry_url: String) -> void:
	"""
	Opens the correct window to display the data at the URL
	
	Returns:
		`true` if able to open the URL in a new window, `false` otherwise
	"""
	var data: Dictionary = {}
	var value_type: String = ""
	var tracker: Dictionary = _async_helper.connect_wrapped(
			SDK.resolve([lbry_url]), "completed")
	yield(_async_helper.wait_until_finished([tracker]), "completed")
	var resolve_response: Dictionary = tracker["result"]
	if resolve_response.has("result"):
		data = resolve_response["result"] \
				.get(resolve_response["result"].keys()[0], {})
	if not data.empty():
		value_type = data.get("value_type", "")
	
	if value_type == "channel":
		_open_channel_window(lbry_url)
		return true
	elif value_type == "stream":
		_open_media_window(data)
		return true
	elif not value_type.empty():
		push_error("Unhandled value type: %s" % value_type)
	return false


func _open_media_window(data: Dictionary) -> void:
	_main_region.set_window("MediaWindow", "init", [data])


func _open_channel_window(channel_url: String) -> void:
	_main_region.set_window("ChannelWindow", "init", [channel_url])


func _on_visibility_changed() -> void:
	"""
	If the this section (Home) is selected and then the user quickly changes
	tabs then switches back to the previous tab, the content of this section
	will not load. This fixes that
	"""
	_make_scrollbar_visible_flag = true


func _on_Results_resized() -> void:
	_make_scrollbar_visible_flag = true


func _on_Results_v_scrollbar_reached_end() -> void:
	""" When the scrollbar reaches the end, load more results """
	_add_results(_last_claim_search_args)


func _on_SearchOptionsButton_pressed() -> void:
	_search_options_container.set_visible(
			not _search_options_container.is_visible())


func _on_SearchBar_text_entered(new_text: String) -> void:
	if _is_ready:
		_apply_search(new_text)


func _on_signal_buffer_timer_timeout() -> void:
	""" Execute the callbacks of the buffered signals """
	if _make_scrollbar_visible_flag:
		_make_scrollbar_visible_flag = false
		_add_results_until_scrollbar_visible()
